#include "QuickJSRuntime.h"

#include <jsi/jsilib.h>
#include <string.h>

#include <regex>
#include <set>
#include <sstream>

#include "HostProxy.h"
#include "JSIValueConverter.h"
#include "Logger.h"
#include "QuickJS/cutils.h"
#include "QuickJS/quickjs-libc.h"
#include "QuickJSPointerValue.h"
#include "ScopedJSValue.h"

namespace qjs {
JSClassID g_hostFunctionClassID;
JSClassID g_hostObjectClassID;
JSClassID g_objectNativeStateClassID;

QuickJSRuntime::QuickJSRuntime() {
  runtime_ = JS_NewRuntime();
  // close stack overflow for multi-thread case
  JS_SetMaxStackSize(runtime_, 0);
  context_ = JS_NewContext(runtime_);
  if (context_ == nullptr) {
    JS_FreeRuntime(runtime_);
  }

  JS_SetRuntimeInfo(runtime_, "QuickJS");
  JS_SetCanBlock(runtime_, true);

  JS_SetModuleLoaderFunc(runtime_, NULL, js_module_loader, NULL); /* set loader for ES6 modules */
  js_std_init_handlers(runtime_);                                 // init event handlers
  js_std_add_helpers(context_, 0, nullptr);                       // add helpers like 'console', 'print'
  js_init_module_os(context_, "os");                              // register module 'os'
  js_init_module_std(context_, "std");                            // register module 'std'
}

void QuickJSRuntime::executePendingJobs() {
  for (;;) {
    JSContext* ctx1;
    int ret = JS_ExecutePendingJob(runtime_, &ctx1);
    if (ret < 0) {
      checkAndThrowException(context_);
    } else if (ret == 0) {
      break;
    }
  }
}

QuickJSRuntime::~QuickJSRuntime() {
  executePendingJobs();

  js_std_free_handlers(runtime_);
  JS_FreeContext(context_);
  JS_FreeRuntime(runtime_);
}

void QuickJSRuntime::checkAndThrowException(JSContext* context) const {
  JSValue exceptionValue = JS_GetException(context);
  if (!JS_IsNull(exceptionValue)) {
    ScopedJSValue scopedJsValue(context, &exceptionValue);
    auto error = jsi::JSError(const_cast<QuickJSRuntime&>(*this), JSIValueConverter::ToJSIValue(*this, exceptionValue));
    logError("checkAndThrowException error: " + error.getMessage() + ", stack: " + error.getStack() + "\n");
    throw error;
  }
}

//
// jsi::Runtime implementations
//
double performanceNow() {
  auto time = std::chrono::steady_clock::now();
  auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(time.time_since_epoch()).count();

  constexpr double NANOSECONDS_IN_MILLISECOND = 1000000.0;
  return duration / NANOSECONDS_IN_MILLISECOND;
}

jsi::Value QuickJSRuntime::evaluateJavaScript(const std::shared_ptr<const jsi::Buffer>& buffer,
                                              const std::string& sourceURL) {
  auto buf_data = buffer->data();
  auto buf_len = buffer->size();
  JSValue obj = JS_ReadObject(context_, buf_data, buf_len, JS_READ_OBJ_BYTECODE);
  JSValue error = JS_GetException(context_);
  if (!JS_IsNull(error)) {
    obj = JS_Eval(context_, (const char*)buf_data, buf_len, sourceURL.c_str(),
                  JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_COMPILE_ONLY);
  }
  JS_FreeValue(context_, error);
  JSValue retValue = JS_EvalFunction(context_, obj);
  ScopedJSValue scopedJsValue(context_, &retValue);
  checkAndThrowException(context_);

  executePendingJobs();
  return JSIValueConverter::ToJSIValue(*this, retValue);
}

std::shared_ptr<const jsi::PreparedJavaScript> QuickJSRuntime::prepareJavaScript(
    const std::shared_ptr<const jsi::Buffer>& buffer, std::string sourceURL) {
  return std::make_shared<facebook::jsi::SourceJavaScriptPreparation>(buffer, std::move(sourceURL));
}

jsi::Value QuickJSRuntime::evaluatePreparedJavaScript(const std::shared_ptr<const jsi::PreparedJavaScript>& js) {
  assert(dynamic_cast<const jsi::SourceJavaScriptPreparation*>(js.get()) &&
         "preparedJavaScript must be a SourceJavaScriptPreparation");
  auto sourceJs = std::static_pointer_cast<const jsi::SourceJavaScriptPreparation>(js);
  return evaluateJavaScript(sourceJs, sourceJs->sourceURL());
}

jsi::Object QuickJSRuntime::global() {
  JSValue global = JS_GetGlobalObject(context_);
  ScopedJSValue scopedJsValue(context_, &global);
  return make<jsi::Object>(new QuickJSPointerValue(runtime_, context_, global));
}

std::string QuickJSRuntime::description() {
  std::ostringstream ss;
  ss << "<QuickJSRuntime@" << this << ">";
  return ss.str();
}

bool QuickJSRuntime::isInspectable() { return false; }

// These clone methods are shallow clone
jsi::Runtime::PointerValue* QuickJSRuntime::cloneSymbol(const Runtime::PointerValue* pv) {
  if (!pv) {
    return nullptr;
  }
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(pv);
  JSValue jsValue = quickJSPointerValue->get(context_);
  assert(JS_IsSymbol(jsValue));
  ScopedJSValue scopedJsValue(context_, &jsValue);

  return new QuickJSPointerValue(runtime_, context_, jsValue);
}

jsi::Runtime::PointerValue* QuickJSRuntime::cloneBigInt(const Runtime::PointerValue* pv) {
  if (!pv) {
    return nullptr;
  }
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(pv);
  JSValue jsValue = quickJSPointerValue->get(context_);
  assert(JS_IsBigInt(context_, jsValue));
  ScopedJSValue scopedJsValue(context_, &jsValue);

  return new QuickJSPointerValue(runtime_, context_, jsValue);
}

jsi::Runtime::PointerValue* QuickJSRuntime::cloneString(const Runtime::PointerValue* pv) {
  if (!pv) {
    return nullptr;
  }

  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(pv);
  JSValue jsValue = quickJSPointerValue->get(context_);
  assert(JS_IsString(jsValue));
  ScopedJSValue scopedJsValue(context_, &jsValue);

  return new QuickJSPointerValue(runtime_, context_, jsValue);
}

jsi::Runtime::PointerValue* QuickJSRuntime::cloneObject(const Runtime::PointerValue* pv) {
  if (!pv) {
    return nullptr;
  }
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(pv);
  JSValue jsValue = quickJSPointerValue->get(context_);
  assert(JS_IsObject(jsValue));
  ScopedJSValue scopedJsValue(context_, &jsValue);

  return new QuickJSPointerValue(runtime_, context_, jsValue);
}

jsi::Runtime::PointerValue* QuickJSRuntime::clonePropNameID(const Runtime::PointerValue* pv) { return cloneString(pv); }

bool QuickJSRuntime::bigintIsInt64(const jsi::BigInt& bigInt) {
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(getPointerValue(bigInt));
  JSValue jsValue = quickJSPointerValue->get(context_);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  int64_t value = 0;
  auto result = !JS_ToBigInt64(context_, &value, jsValue);
  checkAndThrowException(context_);
  return result;
}

bool QuickJSRuntime::bigintIsUint64(const jsi::BigInt& bigInt) {
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(getPointerValue(bigInt));
  JSValue jsValue = quickJSPointerValue->get(context_);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  int64_t value = 0;
  auto result = !JS_ToBigInt64(context_, &value, jsValue) && (value >= 0 && static_cast<uint64_t>(value) <= UINT64_MAX);
  checkAndThrowException(context_);
  return result;
}

uint64_t QuickJSRuntime::truncate(const jsi::BigInt& bigInt) {
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(getPointerValue(bigInt));
  JSValue jsValue = quickJSPointerValue->get(context_);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  uint64_t result;
  JS_ToIndex(context_, &result, jsValue);
  checkAndThrowException(context_);
  return result;
}

bool QuickJSRuntime::hasNativeState(const jsi::Object& object) {
  JSValue jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  auto hostProxy = reinterpret_cast<HostProxy*>(JS_GetOpaque(jsValue, g_objectNativeStateClassID));
  return (hostProxy != nullptr) && hostProxy->hasNativeState();
}

std::shared_ptr<jsi::NativeState> QuickJSRuntime::getNativeState(const jsi::Object& object) {
  JSValue jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  auto hostProxy = reinterpret_cast<HostProxy*>(JS_GetOpaque(jsValue, g_objectNativeStateClassID));
  assert(hostProxy && hostProxy->hasNativeState());
  return static_cast<ObjectNativeState*>(hostProxy)->getNativeState();
}

void QuickJSRuntime::setNativeState(const jsi::Object& object, std::shared_ptr<jsi::NativeState> state) {
  std::call_once(objectNativeStateOnceFlag_, [this]() {
    JS_NewClassID(&g_objectNativeStateClassID);
    JSClassDef objectNativeStateClassDef = {
        "NativeObject",
        .finalizer = ObjectNativeState::finalize,
    };
    JS_NewClass(runtime_, g_objectNativeStateClassID, &objectNativeStateClassDef);
  });

  JSValue jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  JSValue newObject = JS_NewObjectProtoClass(context_, jsValue, g_objectNativeStateClassID);
  JS_SetOpaque(newObject, new ObjectNativeState(*this, state));

  JSValue global = JS_GetGlobalObject(context_);
  JSValue assginFunc = JS_GetPropertyStr(context_, global, "Object.assign");
  JSValue args[2] = {newObject, jsValue};
  JSValue result = JS_Call(context_, assginFunc, JS_UNDEFINED, 2, args);
  JS_FreeValue(context_, result);
  JS_FreeValue(context_, assginFunc);
  JS_FreeValue(context_, global);

  QuickJSPointerValue* quickJSPointerValue =
      static_cast<QuickJSPointerValue*>(const_cast<PointerValue*>(getPointerValue(object)));
  quickJSPointerValue->set(context_, newObject);
}

bool QuickJSRuntime::drainMicrotasks(int maxMicrotasksHint) {
  int taskNum = 0;
  int ret;
  for (;;) {
    if (taskNum < maxMicrotasksHint || maxMicrotasksHint == -1) {
      JSContext* ctx;
      ret = JS_ExecutePendingJob(runtime_, &ctx);
      taskNum++;
      if (ret < 0) {
        checkAndThrowException(context_);
      } else if (ret == 0) {
        return true;
      }
    } else {
      break;
    }
  }
  return false;
}

jsi::PropNameID QuickJSRuntime::createPropNameIDFromAscii(const char* str, size_t length) {
  JSValue jsValue = JS_NewStringLen(context_, str, length);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  QuickJSPointerValue* value = new QuickJSPointerValue(runtime_, context_, jsValue);
  return make<jsi::PropNameID>(value);
}

jsi::PropNameID QuickJSRuntime::createPropNameIDFromUtf8(const uint8_t* utf8, size_t length) {
  JSValue jsValue = JS_NewStringLen(context_, (const char*)utf8, length);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  QuickJSPointerValue* value = new QuickJSPointerValue(runtime_, context_, jsValue);
  return make<jsi::PropNameID>(value);
}

jsi::PropNameID QuickJSRuntime::createPropNameIDFromString(const jsi::String& str) {
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(getPointerValue(str));
  JSValue jsValue = quickJSPointerValue->get(context_);
  ScopedJSValue scopedJsValue(context_, &jsValue);

  assert(JS_IsString(jsValue));

  auto utf8 = JS_ToCString(context_, jsValue);
  auto result = createPropNameIDFromUtf8(reinterpret_cast<const uint8_t*>(utf8), strlen(utf8));
  JS_FreeCString(context_, utf8);
  return result;
}

std::string QuickJSRuntime::utf8(const jsi::PropNameID& sym) {
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(getPointerValue(sym));
  JSValue jsValue = quickJSPointerValue->get(context_);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  return JSIValueConverter::ToSTLString(context_, jsValue);
}

bool QuickJSRuntime::compare(const jsi::PropNameID& a, const jsi::PropNameID& b) {
  std::string aStr = utf8(a);
  std::string bStr = utf8(b);
  return aStr == bStr;
}

std::string QuickJSRuntime::symbolToString(const jsi::Symbol& symbol) {
  return jsi::Value(*this, symbol).toString(*this).utf8(*this);
}

jsi::String QuickJSRuntime::createStringFromAscii(const char* str, size_t length) {
  JSValue jsValue = JS_NewStringLen(context_, str, length);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  QuickJSPointerValue* value = new QuickJSPointerValue(runtime_, context_, jsValue);
  return make<jsi::String>(value);
}

jsi::String QuickJSRuntime::createStringFromUtf8(const uint8_t* str, size_t length) {
  JSValue jsValue = JS_NewStringLen(context_, (const char*)str, length);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  QuickJSPointerValue* value = new QuickJSPointerValue(runtime_, context_, jsValue);
  return make<jsi::String>(value);
}

std::string QuickJSRuntime::utf8(const jsi::String& str) {
  const QuickJSPointerValue* quickJSPointerValue = static_cast<const QuickJSPointerValue*>(getPointerValue(str));
  JSValue jsValue = quickJSPointerValue->get(context_);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  assert(JS_IsString(jsValue));

  return JSIValueConverter::ToSTLString(context_, jsValue);
}

jsi::Object QuickJSRuntime::createObject() {
  JSValue jsValue = JS_NewObject(context_);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  return make<jsi::Object>(new QuickJSPointerValue(runtime_, context_, jsValue));
}

static JSClassExoticMethods hostObjectClassExotic = {
    .get_own_property = HostObjectProxy::getOwnProperty,
    .get_own_property_names = HostObjectProxy::getPropertyNames,
    .get_property = HostObjectProxy::getProperty,
    .set_property = HostObjectProxy::setProperty,
};

jsi::Object QuickJSRuntime::createObject(std::shared_ptr<jsi::HostObject> hostObject) {
  std::call_once(hostObjectClassOnceFlag_, [this]() {
    JS_NewClassID(&g_hostObjectClassID);
    JSClassDef hostObjectClassDef = {
        "HostObject",
        .finalizer = HostObjectProxy::finalize,
        .exotic = &hostObjectClassExotic,
    };
    JS_NewClass(runtime_, g_hostObjectClassID, &hostObjectClassDef);
  });

  JSValue obj = JS_NewObjectClass(context_, g_hostObjectClassID);
  JS_SetOpaque(obj, new HostObjectProxy(*this, hostObject));
  auto res = JSIValueConverter::ToJSIValue(*this, obj).getObject(*this);
  JS_FreeValue(context_, obj);
  checkAndThrowException(context_);
  return res;
}

std::shared_ptr<jsi::HostObject> QuickJSRuntime::getHostObject(const jsi::Object& object) {
  JSValue jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  auto hostProxy = reinterpret_cast<HostProxy*>(JS_GetOpaque(jsValue, g_hostObjectClassID));
  assert(hostProxy && hostProxy->isHostObject());
  return static_cast<HostObjectProxy*>(hostProxy)->getHostObject();
}

jsi::HostFunctionType& QuickJSRuntime::getHostFunction(const jsi::Function& function) {
  JSValue jsValue = JSIValueConverter::ToJSObject(*this, function);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  auto hostProxy = reinterpret_cast<HostProxy*>(JS_GetOpaque(jsValue, g_hostFunctionClassID));
  assert(hostProxy && hostProxy->isHostFunction());
  return static_cast<HostFunctionProxy*>(hostProxy)->getHostFunctionType();
}

jsi::Value QuickJSRuntime::getProperty(const jsi::Object& object, const jsi::PropNameID& name) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  auto jsName = name.utf8(*this);
  auto prop = JS_GetPropertyStr(context_, jsValue, jsName.c_str());
  ScopedJSValue scopeValue(context_, &jsValue);
  ScopedJSValue scopeProp(context_, &prop);

  checkAndThrowException(context_);

  return JSIValueConverter::ToJSIValue(*this, prop);
}

jsi::Value QuickJSRuntime::getProperty(const jsi::Object& object, const jsi::String& name) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  auto jsName = name.utf8(*this);
  auto prop = JS_GetPropertyStr(context_, jsValue, jsName.c_str());
  ScopedJSValue scopeValue(context_, &jsValue);
  ScopedJSValue scopeProp(context_, &prop);

  checkAndThrowException(context_);
  return JSIValueConverter::ToJSIValue(*this, prop);
}

bool QuickJSRuntime::hasProperty(const jsi::Object& object, const jsi::PropNameID& name) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopeValue(context_, &jsValue);
  auto jsName = JS_NewAtom(context_, name.utf8(*this).c_str());
  bool result = JS_HasProperty(context_, jsValue, jsName) == TRUE;
  JS_FreeAtom(context_, jsName);
  return result;
}

bool QuickJSRuntime::hasProperty(const jsi::Object& object, const jsi::String& name) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopeValue(context_, &jsValue);
  auto jsName = JS_NewAtom(context_, name.utf8(*this).c_str());
  bool result = JS_HasProperty(context_, jsValue, jsName) == TRUE;
  JS_FreeAtom(context_, jsName);
  return result;
}

void QuickJSRuntime::setPropertyValue(const jsi::Object& object, const jsi::PropNameID& name, const jsi::Value& value) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  auto jsProperty = JSIValueConverter::ToJSValue(*this, value);
  auto jsName = name.utf8(*this);
  ScopedJSValue scopeValue(context_, &jsValue);

  // DO NOT FREE jsProperty
  JS_SetPropertyStr(context_, jsValue, jsName.c_str(), jsProperty);
  checkAndThrowException(context_);
}

void QuickJSRuntime::setPropertyValue(const jsi::Object& object, const jsi::String& name, const jsi::Value& value) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  auto jsProperty = JSIValueConverter::ToJSValue(*this, value);
  auto jsName = name.utf8(*this);
  ScopedJSValue scopeValue(context_, &jsValue);

  // DO NOT FREE jsProperty
  JS_SetPropertyStr(context_, jsValue, jsName.c_str(), jsProperty);
  checkAndThrowException(context_);
}

bool QuickJSRuntime::isArray(const jsi::Object& object) const {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopeValue(context_, &jsValue);

  return JS_IsArray(context_, jsValue) == TRUE;
}

bool QuickJSRuntime::isArrayBuffer(const jsi::Object& object) const {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopeValue(context_, &jsValue);
  size_t bufSize;
  uint8_t* buf = JS_GetArrayBuffer(context_, &bufSize, jsValue);
  return buf != nullptr;
}

bool QuickJSRuntime::isFunction(const jsi::Object& object) const {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopeValue(context_, &jsValue);
  return JS_IsFunction(context_, jsValue);
}

bool QuickJSRuntime::isHostObject(const jsi::Object& object) const {
  JSValue jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  auto hostProxy = reinterpret_cast<HostProxy*>(JS_GetOpaque(jsValue, g_hostObjectClassID));
  if (!hostProxy) {
    return false;
  }
  return hostProxy->isHostObject();
}

bool QuickJSRuntime::isHostFunction(const jsi::Function& function) const {
  JSValue jsValue = JSIValueConverter::ToJSFunction(*this, function);
  ScopedJSValue scopedJsValue(context_, &jsValue);
  auto hostProxy = reinterpret_cast<HostProxy*>(JS_GetOpaque(jsValue, g_hostFunctionClassID));
  if (!hostProxy) {
    return false;
  }
  return hostProxy->isHostFunction();
}

jsi::Array QuickJSRuntime::getPropertyNames(const jsi::Object& object) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, object);
  ScopedJSValue scopeValue(context_, &jsValue);

  JSPropertyEnum* names = nullptr;
  uint32_t size;
  int ret = JS_GetOwnPropertyNames(context_, &names, &size, jsValue, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK);

  auto result = JS_NewArray(context_);
  ScopedJSValue scopeResult(context_, &result);
  for (int i = 0; i < size; i++) {
    JSValue prop = JS_AtomToValue(context_, names[i].atom);
    // DO NOT FREE prop
    JS_SetPropertyUint32(context_, result, i, prop);
    JS_FreeAtom(context_, names[i].atom);
  }

  if (names) {
    js_free(context_, names);
  }
  checkAndThrowException(context_);
  return make<jsi::Object>(new QuickJSPointerValue(runtime_, context_, result)).getArray(*this);
}

jsi::WeakObject QuickJSRuntime::createWeakObject(const jsi::Object& weakObject) {
  const QuickJSPointerValue* pointer = static_cast<const QuickJSPointerValue*>(getPointerValue(weakObject));
  JSValue value = pointer->get(context_);
  ScopedJSValue scopedJsValue(context_, &value);

  return make<jsi::WeakObject>(new QuickJSPointerValue(runtime_, context_, value));
}

jsi::Value QuickJSRuntime::lockWeakObject(const jsi::WeakObject& weakObject) {
  const QuickJSPointerValue* pointer = static_cast<const QuickJSPointerValue*>(getPointerValue(weakObject));
  JSValue value = pointer->get(context_);
  ScopedJSValue scopedJsValue(context_, &value);
  return JSIValueConverter::ToJSIValue(*this, value);
}

jsi::Array QuickJSRuntime::createArray(size_t length) {
  auto result = JS_NewArray(context_);
  ScopedJSValue scopeResult(context_, &result);
  JSValue len = JS_NewInt64(context_, length);
  JS_SetPropertyStr(context_, result, "length", len);
  return make<jsi::Object>(new QuickJSPointerValue(runtime_, context_, result)).getArray(*this);
}

size_t QuickJSRuntime::size(const jsi::Array& array) {
  auto jsValue = JSIValueConverter::ToJSArray(*this, array);
  ScopedJSValue scopeValue(context_, &jsValue);
  JSValue len = JS_GetPropertyStr(context_, jsValue, "length");
  ScopedJSValue scopeLen(context_, &len);
  return JS_VALUE_GET_INT(len);
}

size_t QuickJSRuntime::size(const jsi::ArrayBuffer& arrayBuffer) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, arrayBuffer);
  ScopedJSValue scopeValue(context_, &jsValue);
  size_t bufSize;
  uint8_t* buf = JS_GetArrayBuffer(context_, &bufSize, jsValue);
  return bufSize;
}

uint8_t* QuickJSRuntime::data(const jsi::ArrayBuffer& arrayBuffer) {
  auto jsValue = JSIValueConverter::ToJSObject(*this, arrayBuffer);
  ScopedJSValue scopeValue(context_, &jsValue);
  size_t bufSize;
  uint8_t* buf = JS_GetArrayBuffer(context_, &bufSize, jsValue);
  return buf;
}

jsi::Value QuickJSRuntime::getValueAtIndex(const jsi::Array& array, size_t i) {
  auto jsValue = JSIValueConverter::ToJSArray(*this, array);
  ScopedJSValue scopeValue(context_, &jsValue);

  JSValue property = JS_GetPropertyUint32(context_, jsValue, i);
  ScopedJSValue scopeProperty(context_, &property);

  checkAndThrowException(context_);
  return JSIValueConverter::ToJSIValue(*this, property);
}

void QuickJSRuntime::setValueAtIndexImpl(const jsi::Array& array, size_t i, const jsi::Value& value) {
  auto jsValue = JSIValueConverter::ToJSArray(*this, array);
  ScopedJSValue scopeValue(context_, &jsValue);

  auto property = JSIValueConverter::ToJSValue(*this, value);
  // DO NOT FREE property
  if (JS_SetPropertyUint32(context_, jsValue, i, property) != TRUE) {
    checkAndThrowException(context_);
  }
}

jsi::Function QuickJSRuntime::createFunctionFromHostFunction(const jsi::PropNameID& name, unsigned int paramCount,
                                                             jsi::HostFunctionType func) {
  std::call_once(hostFunctionClassOnceFlag_, [this]() {
    JS_NewClassID(&g_hostFunctionClassID);
    JSClassDef hostFunctionObjectClassDef = {
        "HostFunctionObject",
        .finalizer = HostFunctionProxy::finalize,
        .call = HostFunctionProxy::call,
    };
    JS_NewClass(runtime_, g_hostFunctionClassID, &hostFunctionObjectClassDef);
  });
  JSValue funcObj = JS_NewObjectClass(context_, g_hostFunctionClassID);
  JS_SetOpaque(funcObj, new HostFunctionProxy(*this, func));

  // set function name property for host function
  JSValue value = JSIValueConverter::ToJSString(*this, name);
  JS_SetPropertyStr(context_, funcObj, "name", value);

  // set arguments lenght property for host function
  JSValue lenValue = JS_NewInt64(context_, paramCount);
  JS_SetPropertyStr(context_, funcObj, "length", lenValue);

  // Make constructor function inherit to Function.prototype
  auto global = JS_GetGlobalObject(context_);
  JSValue functionConstructor = JS_GetPropertyStr(context_, global, "Function");
  JSValue functionPrototype = JS_GetPropertyStr(context_, functionConstructor, "prototype");
  JS_SetPrototype(context_, funcObj, functionPrototype);
  JS_FreeValue(context_, functionPrototype);
  JS_FreeValue(context_, functionConstructor);
  JS_FreeValue(context_, global);

  auto jsiVal = JSIValueConverter::ToJSIValue(*this, funcObj);
  auto res = jsiVal.getObject(*this).getFunction(*this);
  JS_FreeValue(context_, funcObj);
  return res;
}

jsi::Value QuickJSRuntime::call(const jsi::Function& function, const jsi::Value& jsThis, const jsi::Value* args,
                                size_t count) {
  auto jsFunction = JSIValueConverter::ToJSFunction(*this, function);
  ScopedJSValue scopedJsFunction(context_, &jsFunction);

  auto jsObject = jsThis.isUndefined() ? JS_GetGlobalObject(context_) : JSIValueConverter::ToJSValue(*this, jsThis);
  ScopedJSValue scopedJsObject(context_, &jsObject);

  JSValue* argv = new JSValue[count];
  for (size_t i = 0; i < count; i++) {
    argv[i] = JSIValueConverter::ToJSValue(*this, args[i]);
  }
  auto result = JS_Call(context_, jsFunction, jsObject, count, argv);
  ScopedJSValue scopeResult(context_, &result);

  for (size_t i = 0; i < count; i++) {
    JS_FreeValue(context_, argv[i]);
  }
  delete[] argv;
  checkAndThrowException(context_);
  return JSIValueConverter::ToJSIValue(*this, result);
}

jsi::Value QuickJSRuntime::callAsConstructor(const jsi::Function& function, const jsi::Value* args, size_t count) {
  auto jsFunction = JSIValueConverter::ToJSFunction(*this, function);
  ScopedJSValue scopedJsFunction(context_, &jsFunction);

  JSValue* argv = new JSValue[count];
  for (size_t i = 0; i < count; i++) {
    argv[i] = JSIValueConverter::ToJSValue(*this, args[i]);
  }

  auto result = JS_CallConstructor(context_, jsFunction, count, argv);
  ScopedJSValue scopeResult(context_, &result);

  for (size_t i = 0; i < count; i++) {
    JS_FreeValue(context_, argv[i]);
  }
  delete[] argv;
  checkAndThrowException(context_);

  return JSIValueConverter::ToJSIValue(*this, result);
}

bool QuickJSRuntime::strictEquals(const jsi::Symbol& a, const jsi::Symbol& b) const {
  JSValue jsSymbolA = JSIValueConverter::ToJSSymbol(*this, a);
  ScopedJSValue scopedJsSymbolA(context_, &jsSymbolA);
  JSValue jsSymbolB = JSIValueConverter::ToJSSymbol(*this, b);
  ScopedJSValue scopedJsSymbolB(context_, &jsSymbolB);

  return JS_VALUE_GET_PTR(jsSymbolA) == JS_VALUE_GET_PTR(jsSymbolB);
}

bool QuickJSRuntime::strictEquals(const jsi::String& a, const jsi::String& b) const {
  auto aStr = JSIValueConverter::ToJSString(*this, a);
  auto bStr = JSIValueConverter::ToJSString(*this, b);
  ScopedJSValue scopedA(context_, &aStr);
  ScopedJSValue scopedB(context_, &bStr);
  return JSIValueConverter::ToSTLString(context_, aStr) == JSIValueConverter::ToSTLString(context_, bStr);
}

bool QuickJSRuntime::strictEquals(const jsi::Object& a, const jsi::Object& b) const {
  JSValue jsObjectA = JSIValueConverter::ToJSObject(*this, a);
  ScopedJSValue scopedJsObjectA(context_, &jsObjectA);
  JSValue jsObjectB = JSIValueConverter::ToJSObject(*this, b);
  ScopedJSValue scopedJsObjectB(context_, &jsObjectB);

  return JS_VALUE_GET_PTR(jsObjectA) == JS_VALUE_GET_PTR(jsObjectB);
}

bool QuickJSRuntime::instanceOf(const jsi::Object& o, const jsi::Function& f) {
  JSValue jsObjectA = JSIValueConverter::ToJSObject(*this, o);
  ScopedJSValue scopedJsObjectA(context_, &jsObjectA);
  JSValue jsObjectB = JSIValueConverter::ToJSFunction(*this, f);
  ScopedJSValue scopedJsObjectB(context_, &jsObjectB);

  bool result = (JS_IsInstanceOf(context_, jsObjectA, jsObjectB) == TRUE);

  checkAndThrowException(context_);

  return result;
}

jsi::PropNameID QuickJSRuntime::createPropNameIDFromSymbol(const jsi::Symbol& sym) {
  auto str = sym.toString(*this);
  return createPropNameIDFromAscii(str.c_str(), str.length());
}

jsi::ArrayBuffer QuickJSRuntime::createArrayBuffer(std::shared_ptr<jsi::MutableBuffer> buffer) {
  JSValue arrayBuffer = JS_NewArrayBuffer(context_, buffer->data(), buffer->size(), nullptr, nullptr, false);
  return make<jsi::Object>(new QuickJSPointerValue(runtime_, context_, arrayBuffer)).getArrayBuffer(*this);
}

bool QuickJSRuntime::strictEquals(const jsi::BigInt& a, const jsi::BigInt& b) const {
  const QuickJSPointerValue* aPointerValue = static_cast<const QuickJSPointerValue*>(getPointerValue(a));
  JSValue aJSValue = aPointerValue->get(context_);
  ScopedJSValue aScoped(context_, &aJSValue);

  const QuickJSPointerValue* bPointerValue = static_cast<const QuickJSPointerValue*>(getPointerValue(b));
  JSValue bJSValue = bPointerValue->get(context_);
  ScopedJSValue bScoped(context_, &bJSValue);

  int64_t aVal = 0;
  int64_t bVal = 0;
  if (JS_ToBigInt64(context_, &aVal, aJSValue) || JS_ToBigInt64(context_, &bVal, bJSValue)) {
    return false;
  }
  return aVal == bVal;
}

jsi::BigInt QuickJSRuntime::createBigIntFromInt64(int64_t v) {
  return make<jsi::BigInt>(new QuickJSPointerValue(runtime_, context_, JS_NewBigInt64(context_, v)));
}

jsi::BigInt QuickJSRuntime::createBigIntFromUint64(uint64_t v) {
  return make<jsi::BigInt>(new QuickJSPointerValue(runtime_, context_, JS_NewBigUint64(context_, v)));
}

jsi::String QuickJSRuntime::bigintToString(const jsi::BigInt& value, int radix) {
  const QuickJSPointerValue* jsValuePointer = static_cast<const QuickJSPointerValue*>(getPointerValue(value));
  JSValue jsValue = jsValuePointer->get(context_);
  ScopedJSValue scopedJsValue(context_, &jsValue);

  JSValue toStringFunc = JS_GetPropertyStr(context_, jsValue, "toString");
  ScopedJSValue scopedJsFunction(context_, &toStringFunc);

  JSValue radixValue = JS_NewInt32(context_, radix);
  ScopedJSValue scopedJsRadix(context_, &radixValue);

  JSValue ret = JS_Call(context_, toStringFunc, jsValue, 1, &radixValue);
  assert(JS_IsString(ret));

  return make<jsi::String>(new QuickJSPointerValue(runtime_, context_, ret));
}
}  // namespace qjs
